昨天提到關於 Single Responsibility Principle 原則。今天想跟大家聊聊 SOLID 原則中的第二個原則 —— 開放封閉原則(Open-Closed Principle, OCP)。這個原則最初由 Bertrand Meyer 提出,很常遇到的情況是例如一個元件被許多地方共用,當我們修改這個元件的時候就會思考是否會改到其他已經正在使用這個元件的地方導致程式壞掉?因此我會選擇擴充而非修改的方式,透過這樣的方式可以在不影響現有功能的前提下,安全地新增新功能。
接下來我們會透過一個電商折扣計算系統的 TypeScript 範例,來了解如何實踐這個原則。
下面用「動物園」情境示範開放封閉原則(Open–Closed Principle, OCP)。不好的範例會用 switch/if 管理各動物邏輯;好的範例則透過介面與多型,對「擴充」開放、對「修改」封閉。
下面用「動物園」情境示範開放封閉原則(Open–Closed Principle, OCP)。不好的範例會用 switch/if 管理各動物邏輯;好的範例則透過介面與多型,對「擴充」開放、對「修改」封閉。
enum AnimalType {
Dog = "dog",
Cat = "cat",
Monkey = "monkey",
}
type Animal = { type: AnimalType; name: string };
function makeSound(a: Animal) {
switch (a.type) {
case AnimalType.Dog:
console.log(`${a.name}: Woof!`);
break;
case AnimalType.Cat:
console.log(`${a.name}: Meow~`);
break;
case AnimalType.Monkey:
console.log(`${a.name}: Ooh-ooh-aah-aah!`);
break;
}
}
function feed(a: Animal, food: string) {
switch (a.type) {
case AnimalType.Dog:
console.log(`${a.name} eats ${food}.`);
break;
case AnimalType.Cat:
console.log(`${a.name} nibbles ${food}.`);
break;
case AnimalType.Monkey:
console.log(`${a.name} loves ${food}!`);
break;
}
}
const zoo: Animal[] = [
{ type: AnimalType.Dog, name: "Fido" },
{ type: AnimalType.Cat, name: "Mimi" },
{ type: AnimalType.Monkey, name: "Kiki" },
];
for (const a of zoo) {
makeSound(a);
feed(a, "banana");
}
interface Animal {
name: string;
makeSound(): void;
feed(food: string): void;
}
class Dog implements Animal {
constructor(public name: string) {}
makeSound() {
console.log(`${this.name}: Woof!`);
}
feed(food: string) {
console.log(`${this.name} eats ${food}.`);
}
}
class Cat implements Animal {
constructor(public name: string) {}
makeSound() {
console.log(`${this.name}: Meow~`);
}
feed(food: string) {
console.log(`${this.name} nibbles ${food}.`);
}
}
class Monkey implements Animal {
constructor(public name: string) {}
makeSound() {
console.log(`${this.name}: Ooh-ooh-aah-aah!`);
}
feed(food: string) {
console.log(`${this.name} loves ${food}!`);
}
}
class Duck implements Animal {
constructor(public name: string) {}
makeSound() {
console.log(`${this.name}: Quack!`);
}
feed(food: string) {
console.log(`${this.name} gobbles ${food}.`);
}
}
function runZoo(animals: Animal[]) {
for (const a of animals) {
a.makeSound();
a.feed("banana");
}
}
const zoo2: Animal[] = [
new Dog("Fido"),
new Cat("Mimi"),
new Monkey("Kiki"),
new Duck("Daisy"),
];
runZoo(zoo2);
透過上面兩個例子可以發現好的範例以 Animal 介面抽象行為,流程只依賴多型,不看具體類別。要新增動物時,只新增一個實作(如 Duck),既有流程 runZoo 完全不改。此設計對擴充開放、對修改封閉,降低風險、提高可測性與可維護性。
不好的範例把各動物邏輯塞進 switch/if,流程緊耦合具體型別。每新增一種動物,就得在多處分支同步改 makeSound、feed,容易造成漏改。